iT邦幫忙

2026 iThome 鐵人賽

DAY 29
0
AI Engineering

從零搞懂 RAG 評測之旅系列 第 29 篇

DAY29. 錯誤分析與調參:用 baseline 驗證改動有沒有用

  • 分享至 

  • xImage
  •  

前幾天我們建好了 RAG、四個通用指標和四個自訂指標。今天是整個系列的收尾,要用這些指標做真正的工程工作:先找出系統哪裡不好、判斷問題出在檢索還是生成,再調整參數,最後用數字驗證改動到底有沒有用。

今日目標

  1. 把 RAG 的設定包成可重複執行的「實驗」
  2. 做錯誤分析:判斷低分題目是檢索失敗還是生成問題
  3. 一次只改一個參數(k、chunk_size),跑出三組實驗
  4. 與 baseline 比較,並用「雜訊參考」判斷差距是否有意義
  5. 回顧整個系列,並整理從原型走向產品的下一步

觀念:調參前先診斷

沒有目標的調參,很容易變成亂槍打鳥。正確的順序是:

跑 baseline → 錯誤分析(找出問題在哪)→ 提出假設 → 改一個參數 → 重新評估 → 比較

Step 1:把設定包成實驗

為了讓每次調整都能重複、可比較,新建 experiment.py,把一組設定(chunk_size、chunk_overlap、k)定義成一個「實驗」:

EXPERIMENTS = {
    "baseline": {"chunk_size": 500,  "chunk_overlap": 50,  "k": 4},
    "k6":       {"chunk_size": 500,  "chunk_overlap": 50,  "k": 6},
    "cs250":    {"chunk_size": 250,  "chunk_overlap": 25,  "k": 4},
    "cs1000":   {"chunk_size": 1000, "chunk_overlap": 100, "k": 4},
}

每次只改一個變因:k6 只改 k,cs250 與 cs1000 只改 chunk_size。如果同時改兩個,分數變了你就不知道是哪個造成的。

每個實驗會建立自己專屬的向量庫(collection 名稱為 exp-實驗名稱),這樣不同的切割方式不會互相覆蓋:

def get_experiment_store(name, cfg):
    store = Chroma(
        collection_name=f"exp-{name}",
        embedding_function=get_embeddings(),
        persist_directory=PERSIST_DIR,
        collection_configuration={"hnsw": {"space": "cosine"}},
    )
    if not store.get()["ids"]:  # 還沒建過才建庫
        chunks = split_documents(load_documents(), cfg["chunk_size"], cfg["chunk_overlap"])
        store.add_documents(chunks)
    return store

這個函式有快取:已經建過的實驗庫不會重建。但這也意味著如果你修改了某個實驗的參數,必須改實驗名稱,否則會讀到舊參數建的庫。

以我們的 5 份範例文件為例,三種切割大小產生的 chunk 數量分別是:

chunk_size 250 500 1000
chunk 總數 28 13 5

注意 chunk_size=1000 時全庫只剩 5 個 chunks,而 k=4,等於每次幾乎把整個知識庫都撈回來。這會對檢索精確度造成什麼影響,是今天實驗的一個看點。

Step 2:新增一個「檢索命中率」

LLM 評審的分數有雜訊,但檢索有一個完全確定、不會有雜訊的檢查方式:測試集每題都標了 source(答案應該出自哪份文件),我們直接看撈回來的 chunks 有沒有來自那份文件:

def run_chain(chain, testset):
    rows = []
    for i, item in enumerate(testset, 1):
        start = time.perf_counter()
        result = chain.invoke(item["question"])
        latency = time.perf_counter() - start
        sources = [os.path.basename(d.metadata["source"]) for d in result["context"]]
        rows.append({
            **item,
            "answer": result["answer"],
            "contexts": [d.page_content for d in result["context"]],
            "retrieved_sources": sources,
            "source_hit": item["source"] in sources,
            "latency": round(latency, 2),
        })
        print(f"[{i}/{len(testset)}] {latency:5.1f}s | 命中={rows[-1]['source_hit']} | {item['question']}")
    return rows

source_hit 為 True 代表至少有一個 chunk 來自正確的文件。它跟昨天的「規則型指標」是同一個精神:能用規則判斷的,就不要交給評審模型。當你要確認「調整檢索參數有沒有讓檢索變好」,這個數字比 Context Recall 可靠得多。

Step 3:執行 baseline,並用它做錯誤分析

為什麼 baseline 要重跑,而不直接用 DAY27 的結果?兩個理由:DAY27 的輸出沒有記錄 retrieved_sources,無法判斷命中;而且重跑之後,我們還能拿它跟 DAY27 的分數比較,量出評審模型本身的雜訊有多大,後面會用到。

執行:

uv run python experiment.py run baseline

這會跑 20 題生成、5 題知識庫外問題,以及完整的 RAGAS 評分,耗時與 DAY27 相當,請預留時間。跑完會產生 eval/exp_baseline.csv 與 eval/experiments.csv。

接著執行錯誤分析:

uv run python experiment.py analyze baseline

診斷邏輯寫在 diagnose() 裡,依序判斷:

RECALL_LOW = 0.5
FAITHFULNESS_LOW = 0.7
RELEVANCY_LOW = 0.5


def diagnose(row):
    if not row["source_hit"]:
        return "檢索失敗:沒撈到正確文件"
    if pd.notna(row["context_recall"]) and row["context_recall"] < RECALL_LOW:
        return "檢索不足:撈到文件但缺關鍵內容"
    if pd.notna(row["faithfulness"]) and row["faithfulness"] < FAITHFULNESS_LOW:
        return "生成問題:答案超出參考資料"
    if pd.notna(row["answer_relevancy"]) and row["answer_relevancy"] < RELEVANCY_LOW:
        return "生成問題:答非所問"
    if row[METRIC_NAMES].isna().any():
        return "無法判斷:有指標評分失敗(NaN)"
    return "正常"

判斷順序是刻意的:先檢索、後生成。 檢索都失敗了,後面的生成分數沒有討論意義。這些門檻是經驗法則,請依你自己 baseline 的分數分布調整。

分析會輸出三樣東西:

  1. 問題分類統計:20 題各屬於哪一類,一眼看出主要病灶
  2. 各題型的平均分數:看「事實、比較、細節」哪一種題型最弱
  3. 需要人工檢查的題目清單:列出題目、診斷、「應出自哪份文件 vs 實際撈到哪些文件」,以及回答的前 80 字

請務必親眼看過清單裡的每一題。 自動診斷只是幫你縮小範圍,真正的原因要看內容才知道:是 chunk 把關鍵句切斷了?是兩份文件主題相近而混淆了?還是標準答案寫得有問題?有時候低分的原因出在測試集,而不是系統。

Step 4:提出假設,跑實驗

看完錯誤分析,你會有一些假設。下面是常見的對應,請依你實際看到的分類挑選:

你看到的現象 假設 對應的實驗
「檢索不足」居多,尤其是比較題 比較題需要撈到兩邊的資訊,k 太小 k6
「檢索失敗」居多,撈到的 chunks 很零碎 chunk 太小,語意破碎 cs1000
Context Precision 偏低,撈到很多雜訊 chunk 太大,一段混了多個主題 cs250
「生成問題」居多 檢索沒問題,應該改 prompt,而不是調這三個參數 不跑這些實驗

最後一列很重要:如果診斷顯示主要是生成問題,今天這三個實驗就幫不上忙,該回頭改 DAY26 的 prompt。不要因為實驗已經寫好了就硬跑。

執行你想驗證的實驗(可以挑一到三個):

uv run python experiment.py run k6
uv run python experiment.py run cs250
uv run python experiment.py run cs1000

每個實驗都是完整的一輪生成與評分,建議分開跑,甚至放著讓它在背景跑。

Step 5:比較結果,並判斷差距有沒有意義

uv run python experiment.py compare

這會輸出三張表:

1. 實驗比較表:每個實驗的設定、四個 RAGAS 指標、檢索命中率、三個自訂規則型指標(繁體中文合規率、誤拒率、正確拒答率)、平均回應時間與 NaN 總數。

2. 與 baseline 的差距:每個指標比 baseline 高多少或低多少。

3. 雜訊參考:DAY27 的 baseline 與今天重跑的 baseline,在四個指標上差多少。


上一篇
DAY28. 自訂評估指標
下一篇
DAY30. RAG 30 天總結
系列文
從零搞懂 RAG 評測之旅 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言